package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enmus.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author 晚风
 * @since 2023-09-29
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCouponMqServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService exchangeCodeService;
    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;
    private final RabbitMqHelper rabbitMqHelper;
    private final ICouponScopeService couponScopeService;
    private final Executor discountSolutionExecutor;

    // 领取优惠券
    @Override
    @MyLock(name = "lock:coupon:uid:#{id}")
//    @Transactional
    public void receiveCoupon(Long id) {
        // 1. 根据id查询优惠券信息  做相关校验
        if (id == null) {
            throw new BadRequestException("非法参数");
        }
//        Coupon coupon = couponMapper.selectById(id);
        // 从redis获取优惠券信息
        Coupon coupon = queryCouponByCache(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
//        if (coupon.getStatus() != CouponStatus.ISSUING) {
//            throw new BadRequestException("该优惠券状态不是在发放");
//        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券已过期或未发放");
        }
//        if (coupon.getTotalNum() <= 0 || coupon.getIssueNum() >= coupon.getTotalNum()) {
        if (coupon.getTotalNum() <= 0) {
            throw new BadRequestException("该优惠券库存不足");
        }
        Long userId = UserContext.getUser();
        // 获取当前用户对该优惠券已领取数量
/*        Integer count = this.lambdaQuery().eq(UserCoupon::getUserId, userId).eq(UserCoupon::getCouponId, id).count();
        if (count == null || count >= coupon.getUserLimit()){
            throw new BadRequestException("以达到领取上限");
        }
        // 2. 优惠券的已发放数量 + 1
        couponMapper.incrIssueNum(id);
        // 3. 生成用户卷
        saveUserCoupon(userId,coupon);*/
//        checkAndCreateUserCoupon(userId,coupon,null);

        // intern  表示强制从常量池中取数据  这样可以保证 同一个用户只能拿到一个锁
//            synchronized (userId.toString().intern()){
//                checkAndCreateUserCoupon(userId, coupon, serialNum);
//            }
//        synchronized (userId.toString().intern()){
//            // 从aop中获取当前对象的代理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null); // 使用代理对象调用方法  方法是有事务处理的
//        }
        // 通过工具类实现分布式锁
//        String key = "lock:coupon:uid:" + userId;
//        RedisLock isLock = new RedisLock(key, redisTemplate);
//        try {
//            boolean lockBool = isLock.tryLock(5, TimeUnit.SECONDS);
//            if (!lockBool) {
//                throw new BizIllegalException("操作太频繁了");
//            }
//            // 从aop中获取当前对象的代理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null); // 使用代理对象调用方法  方法是有事务处理的
//        } finally {
//            isLock.unlock();
//        }

        // 通过redisson实现分布式锁
//        String key = "lock:coupon:uid:" + userId;
        // 从aop中获取当前对象的代理对象
        // 统计已领数量
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + id;
        // increment 表示本次领取完后的领取数量
        Long increment = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
        // 校验是否超过限领数量
        if (increment > coupon.getUserLimit()) {
            throw new BizIllegalException("已领取该优惠券最大值");
        }
        // 修改优惠券的库存 -1
        String couponKey = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        redisTemplate.opsForHash().increment(couponKey, "totalNum", -1);
        // 发送消息到mq 消息内容 用户id  优惠券id
        UserCouponDTO msg = new UserCouponDTO();
        msg.setCouponId(id); // 优惠券id
        msg.setUserId(userId); // 用户id
        rabbitMqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, msg);
//        IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
//        userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null); // 使用代理对象调用方法  方法是有事务处理的
    }

    /**
     * 从redis中获取优惠券信息（领取开始时间和结束时间、用户限领数量、优惠券总数量）
     *
     * @param id
     * @return
     */
    private Coupon queryCouponByCache(Long id) {
        // 拼接key
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        // 从redis中获取数据
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        Coupon coupon = BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());
        return coupon;
    }

    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        // 1. 判断code是否为空
        if (StringUtils.isBlank(code)) {
            throw new BadRequestException("非法参数");
        }
        // 2. 解析兑换码获取自增id
        Long serialNum = CodeUtil.parseCode(code);
        // 3. 判断是否已兑换 使用redis的bitmap结构 setbit key offset 1 如果方法返回true表示兑换码已兑换
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum, true);
        if (result) {
            // 表示兑换码已被兑换
            throw new BizIllegalException("兑换码已被兑换");
        }
        try {
            // 4. 判断是否存在
            ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            // 5. 判断兑换码是否过期
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            if (now.isAfter(expiredTime)) {
                throw new BizIllegalException("兑换码已过期");
            }
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            Long userId = UserContext.getUser();
            if (coupon == null) {
                throw new BizIllegalException("优惠券不存在");
            }
            checkAndCreateUserCoupon(userId, coupon, serialNum);
        } catch (Exception e) {
            // 将兑换码状态重置
            exchangeCodeService.updateExchangeCodeMark(serialNum, false);
            throw e;
        }

    }

    @MyLock(name = "lock:coupon:uid:#{userId}", lockType = MyLockType.RE_ENTRANT_LOCK, lockStrategy = MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT)
    @Transactional
    public void checkAndCreateUserCoupon(Long userId, Coupon coupon, Long serialNum) {
        synchronized (userId.toString().intern()) {
            Integer count = this.lambdaQuery()
                    .eq(UserCoupon::getUserId, userId)
                    .eq(UserCoupon::getCouponId, coupon.getId()).count();
            if (count == null || count >= coupon.getUserLimit()) {
//                throw new BadRequestException("以达到领取上限");
                return;
            }
            // 2. 优惠券的已发放数量 + 1
            couponMapper.incrIssueNum(coupon.getId());
            // 3. 生成用户卷
            saveUserCoupon(userId, coupon);
            // 4. 更新兑换码的状态
            if (serialNum != null) {
                exchangeCodeService.lambdaUpdate()
                        .set(ExchangeCode::getUserId, userId)
                        .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                        .eq(ExchangeCode::getId, serialNum)
                        .update();
            }
        }
    }

    @Transactional
    @Override
    public void checkAndCreateUserCouponNew(UserCouponDTO msg) {
//            Integer count = this.lambdaQuery()
//                    .eq(UserCoupon::getUserId, userId)
//                    .eq(UserCoupon::getCouponId, coupon.getId()).count();
//            if (count == null || count >= coupon.getUserLimit()) {
//                throw new BadRequestException("以达到领取上限");
//            }
        // 1. 查询db
        Coupon coupon = couponMapper.selectById(msg.getCouponId());
        if (coupon == null) {
            return;
        }
        // 2. 优惠券的已发放数量 + 1
        couponMapper.incrIssueNum(coupon.getId());
        // 3. 生成用户卷
        saveUserCoupon(msg.getUserId(), coupon);
//            // 4. 更新兑换码的状态
//            if (serialNum != null) {
//                exchangeCodeService.lambdaUpdate()
//                        .set(ExchangeCode::getUserId, userId)
//                        .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
//                        .eq(ExchangeCode::getId, serialNum)
//                        .update();
//            }
    }
    // 保存用户卷
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(coupon.getId());
        LocalDateTime issueBeginTime = coupon.getIssueBeginTime(); // 优惠券使用开始时间
        LocalDateTime issueEndTime = coupon.getIssueEndTime();  // 优惠券使用截止时间
        if (issueBeginTime == null && issueEndTime == null) {  // 判断是否有传时间 如果没有传 就把当前时间给到开始时间  把有效期的天数 加到结束时间
            issueBeginTime = LocalDateTime.now();
            issueEndTime = issueBeginTime.plusDays(coupon.getTermDays());
        }
        coupon.setIssueBeginTime(issueBeginTime);
        coupon.setIssueEndTime(issueEndTime);
        this.save(userCoupon);
    }

    /**
     * 查询可用优惠券方案
     * @param courses
     * @return
     */
    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> courses) {
        // 1.查询当前用户可用的优惠券  coupon 和user_coupon表  条件：userid、status=1  查询哪些字段  优惠券的规则 优惠券id  用户卷id
        List<Coupon> coupons = getBaseMapper().queryMyCoupons(UserContext.getUser());
        if (CollUtils.isEmpty(coupons)){
            return CollUtils.emptyList();
        }
        // 2. 初筛
        // 2.1 计算订单总金额  对course 的price累加
        int totalAmount = courses.stream().mapToInt(OrderCourseDTO::getPrice).sum();

        // 2.2 校验优惠券是否可用
        List<Coupon> availableCoupons = coupons.stream()
                .filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon)).collect(Collectors.toList());
        if (CollUtils.isEmpty(availableCoupons)){
            return CollUtils.emptyList();
        }
        // 3. 细筛 (需要考虑优惠券的先锋范围) 排列组合
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons,courses);
        if (avaMap.isEmpty()){
            return CollUtils.emptyList();
        }
        availableCoupons = new ArrayList<>(avaMap.keySet()); // 真正可用的优惠券集合
        // 排列组合
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons); // 组合卷
        for (Coupon availableCoupon : availableCoupons) {
            solutions.add(List.of(availableCoupon)); // 添加单卷到集合中
        }

        // 4. 计算每一种组合的优惠明细
/*        List<CouponDiscountDTO> dtos = new ArrayList<>();
        for (List<Coupon> solution : solutions) {
            CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
            log.debug("方案最终优惠 {} 方案中优惠券使用了 {} 规则  {}",dto.getDiscountAmount(),dto.getIds(),dto.getRules());
            dtos.add(dto);
        }*/
        // 5. 使用多线程改造第四步 并进行计算每一种组合的优惠明细
        log.debug("多线程计算每一种组合的优惠详细");
//        List<CouponDiscountDTO> dtos = new ArrayList<>(); // 线程不安全
        List<CouponDiscountDTO> dtos = Collections.synchronizedList(new ArrayList<>()); // 线程安全集合
        CountDownLatch latch = new CountDownLatch(solutions.size()); // 计数器
        for (List<Coupon> solution : solutions) {
            CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
                /**
                 * Gets a result.
                 *
                 * @return a result
                 */
                @Override
                public CouponDiscountDTO get() {
                    CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
                    return dto;
                }
            },discountSolutionExecutor).thenAccept(new Consumer<CouponDiscountDTO>() {
                @Override
                public void accept(CouponDiscountDTO dto) {
                    log.debug("方案最终优惠 {} 方案中优惠券使用了 {} 规则  {}",dto.getDiscountAmount(),dto.getIds(),dto.getRules());
                    dtos.add(dto);
                    latch.countDown(); // 计数器减一
                }
            });
        }
        try {
            latch.await(3, TimeUnit.SECONDS); // 要么等计数器归零  要么等三秒 执行
        } catch (InterruptedException e) {
            log.error("多线程计算组合优惠报错了",e);
        }
        // 6. 筛选最优解
        return findBestSolution(dtos);
    }

    /**
     * 求最优解
     * - 用卷相同时， 优惠金额最高的方案
     * - 优惠金额相同时， 用卷最少的方案
     * @param solutions
     * @return
     */
    private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
        // 1. 创建两个map 分别记录用卷相同，金额最高    金额相同， 用卷最少
        Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();
        // 2. 循环方案  向map中记录  有卷相同， 金额最高   金额相同 ，用卷最少
        for (CouponDiscountDTO solution : solutions) {
            // 2.1 对优惠券id升序，转字符串  然后以逗号拼接
            String ids = solution.getIds().stream().sorted(Comparator.comparing(Long::longValue)).map(String::valueOf).collect(Collectors.joining(","));
            // 2.2 从moreDiscountMap中取出旧的方案  判断 当前优惠方案的优惠金额  是否小于就的方案  小于 忽略当前方案保留旧的方案  处理下一个方案
            CouponDiscountDTO old = moreDiscountMap.get(ids);
            if (old != null && old.getDiscountAmount() >= solution.getDiscountAmount()){
                continue;
            }
            // 2.3 从lessCouponMap中取出旧的方案  判断当前方案用卷数量大于 旧的方案当前方案忽略 保留旧的方案 处理下一个方案
            old = lessCouponMap.get(solution.getDiscountAmount());
            int newSize = solution.getIds().size(); // 新方案中的用卷数量
            if (old != null && newSize > 1 && old.getIds().size() <= newSize){
                continue;
            }
            // 2.4 添加更优方案到map中
            moreDiscountMap.put(ids,solution); //
            lessCouponMap.put(solution.getDiscountAmount(),solution);
        }
        // 3. 求两个map的交集
        Collection<CouponDiscountDTO> bastSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 4. 对最终的方案结果  按优惠金额  倒序
        List<CouponDiscountDTO> latestBastSolution = bastSolution.stream()
                .sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());
        return latestBastSolution;
    }

    /**
     * 计算每一个方案的优惠信息
     * @param avaMap  // 优惠券和可用课程的映射集合
     * @param courses // 订单中所有的课程
     * @param solution // 方案
     * @return
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> avaMap, List<OrderCourseDTO> courses, List<Coupon> solution) {
        // 1. 创建方案结果dto对象
        CouponDiscountDTO dto = new CouponDiscountDTO();
        // 2. 初始化商品id和商品折扣明细的映射， 初始折扣明细全部设置为0
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, orderCourseDTO -> 0));
        // 3. 计算该方案的折扣信息
        // 3.1 循环方案中优惠券
        for (Coupon coupon : solution) {
            // 3.2 取出该优惠券对应的可用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            // 3.3 计算可用课程的总金额（商品价格 - 该商品的折扣明细）
            int totalAmount = availableCourses.stream().mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            // 3.4 判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (!discount.canUse(totalAmount,coupon)){
                continue; // 卷不可用跳出当前循环，继续下一次循环
            }
            // 3.5 计算该优惠券使用后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.6 更新商品的折扣明细（更新商品id的商品折扣明细）
            calculateDetailDiscount(detailMap, availableCourses, totalAmount,discountAmount);
            // 3.7 累加每一个优惠券的优惠金额 赋值给方案结果dto对象
            dto.getIds().add(coupon.getId()); // 只要执行当前这句代码，就代表优惠券生效
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount()); // 不能覆盖  应该是所有生效的优惠券累加的结果
        }
        return dto;
    }

    /**
     * 计算商品  折扣明细
     * @param detailMap  商品id和商品的优惠明细 映射
     * @param availableCourses  当前优惠券可用 的课程集合
     * @param totalAmount 可用的课程的总金额
     * @param discountAmount 当前优惠券能优惠的金额
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap, List<OrderCourseDTO> availableCourses, int totalAmount, int discountAmount) {
        // 目的： 本方法就是优惠券在使用后  计算每个商品的折扣明细
        // 规则： 前面的商品按比例计算， 最后一个商品折扣明细 = 总的优惠金额 - 前面商品优惠的总额
        int times = 0;
        int remainDiscount = discountAmount;
        for (OrderCourseDTO c : availableCourses) {
            times++;
            int discount = 0;
            if (times == availableCourses.size()){
                // 是最后一个，最后一个商品折扣明细 = 总的优惠金额 - 前面商品优惠的总额
                discount = remainDiscount;
            }else {
                // 不是最后一个按照比例计算
                discount = c.getPrice() * discountAmount / totalAmount; // 这里需要先乘在除  不然是0
                remainDiscount -= discount;
            }
            detailMap.put(c.getId(),discount + detailMap.get(c.getId()));
        }
    }

    /**
     * 细筛，查询每一个优惠券  对应可用的课程
     * @param coupons  // 粗筛过后的优惠券集合
     * @param orderCourses // 订单中的课程集合
     * @return
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon, List<OrderCourseDTO>> map = new HashMap<>();
        // 1. 循环遍历初筛后的优惠券集合
        for (Coupon coupon : coupons) {
            List<OrderCourseDTO> availableCourses = orderCourses;
            // 2. 找出每一个优惠券的可用课程
            // 2.1 判断优惠券是否限定了范围 coupon.specific 为true
            if (coupon.getSpecific()){
                // 2.2 查询限定范围  查询coupon_scope表，条件coupon_id
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 2.3 得到限定范围的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                // 2.4 从orderCourses 订单中所有的课程集合  筛选 该范围内的课程
                availableCourses = orderCourses.stream()
                        .filter(orderCourseDTO -> scopeIds.contains(orderCourseDTO.getCateId())).collect(Collectors.toList());
            }
            if (CollUtils.isEmpty(availableCourses)){
                continue; // 说明当前优惠券限定了范围  但是在订单中的课程中没有找到可用课程，说明改卷不可用， 忽略改卷，进行下一个优惠券处理
            }
            // 3. 计算该优惠券  可用课程的总金额
            int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            // 4. 判断该优惠券是否可用  如果可用添加到map中
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalAmount,coupon)){
                map.put(coupon,availableCourses);
            }
        }
        return map;
    }
}
